package edu.washington.cs.sounddetector.server.sound;

import jAudioFeatureExtractor.Aggregators.Aggregator;
import jAudioFeatureExtractor.Aggregators.AggregatorContainer;
import jAudioFeatureExtractor.AudioFeatures.FeatureExtractor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang3.ArrayUtils;

/**
 * <p>
 * SoundProcessorBuilder is used to build SoundProcessor objects.
 * </p>
 * 
 * <p>
 * SoundProcessorBuilder is mutable and not threadsafe.
 * </p>
 * 
 * @author the.dylan.price@gmail.com
 */
public class SoundProcessorBuilder
{

    /**
     * All features jAudio is able to extract.
     */
    private static final FeatureExtractor[] allFeatures;

    /**
     * A Map from feature names to their index in the allFeatures array.
     */
    private static final Map<String, Integer> featureIndexes;

    /**
     * A Map from aggregator names to the Aggregator.
     */
    private static final Map<String, Aggregator> aggregatorIndexes;

    /**
     * Static initialization of allFeatures and featureIndexes.
     */
    static
    {
        allFeatures = new FeatureExtractor[] {
                new jAudioFeatureExtractor.AudioFeatures.MagnitudeSpectrum(),
                new jAudioFeatureExtractor.AudioFeatures.PowerSpectrum(),
                new jAudioFeatureExtractor.AudioFeatures.FFTBinFrequencies(),
                new jAudioFeatureExtractor.AudioFeatures.SpectralCentroid(),
                new jAudioFeatureExtractor.AudioFeatures.SpectralRolloffPoint(),
                new jAudioFeatureExtractor.AudioFeatures.SpectralFlux(),
                new jAudioFeatureExtractor.AudioFeatures.Compactness(),
                new jAudioFeatureExtractor.AudioFeatures.SpectralVariability(),
                new jAudioFeatureExtractor.AudioFeatures.RMS(),
                new jAudioFeatureExtractor.AudioFeatures.FractionOfLowEnergyWindows(),
                new jAudioFeatureExtractor.AudioFeatures.ZeroCrossings(),
                new jAudioFeatureExtractor.AudioFeatures.BeatHistogram(),
                new jAudioFeatureExtractor.AudioFeatures.BeatHistogramLabels(),
                new jAudioFeatureExtractor.AudioFeatures.StrongestBeat(),
                new jAudioFeatureExtractor.AudioFeatures.BeatSum(),
                new jAudioFeatureExtractor.AudioFeatures.StrengthOfStrongestBeat(),
                new jAudioFeatureExtractor.AudioFeatures.StrongestFrequencyViaZeroCrossings(),
                new jAudioFeatureExtractor.AudioFeatures.StrongestFrequencyViaSpectralCentroid(),
                new jAudioFeatureExtractor.AudioFeatures.StrongestFrequencyViaFFTMax(),
                new jAudioFeatureExtractor.AudioFeatures.MFCC(),
                new jAudioFeatureExtractor.AudioFeatures.ConstantQ(),
                new jAudioFeatureExtractor.AudioFeatures.LPC(),
                new jAudioFeatureExtractor.AudioFeatures.Moments(),
                new jAudioFeatureExtractor.AudioFeatures.PeakFinder(),
                new jAudioFeatureExtractor.AudioFeatures.HarmonicSpectralCentroid(),
                new jAudioFeatureExtractor.AudioFeatures.HarmonicSpectralFlux(),
                new jAudioFeatureExtractor.AudioFeatures.HarmonicSpectralSmoothness(),
                new jAudioFeatureExtractor.AudioFeatures.RelativeDifferenceFunction(),
                new jAudioFeatureExtractor.AudioFeatures.AreaMoments(),
                new jAudioFeatureExtractor.AudioFeatures.AreaMomentsMFCC(),
                new jAudioFeatureExtractor.AudioFeatures.AreaMomentsLogConstantQ(),
                new jAudioFeatureExtractor.AudioFeatures.AreaMomentsConstantQMFCC(),
                new jAudioFeatureExtractor.AudioFeatures.ConstantQ(),
                new jAudioFeatureExtractor.AudioFeatures.LogConstantQ(),
                new jAudioFeatureExtractor.AudioFeatures.ConstantQMFCC(),
                new jAudioFeatureExtractor.AudioFeatures.AreaPolynomialApproximation(),
                new jAudioFeatureExtractor.AudioFeatures.AreaPolynomialApproximationConstantQMFCC(),
                new jAudioFeatureExtractor.AudioFeatures.AreaPolynomialApproximationLogConstantQ() };

        featureIndexes = new HashMap<String, Integer>();
        for (int i = 0; i < allFeatures.length; i++)
        {
            featureIndexes.put(allFeatures[i].getFeatureDefinition().name, i);
        }

        aggregatorIndexes = new HashMap<String, Aggregator>();
        List<Aggregator> aggs = new ArrayList<Aggregator>();
        aggs.add(new jAudioFeatureExtractor.Aggregators.AreaMoments());
        aggs.add(new jAudioFeatureExtractor.Aggregators.AreaPolynomialApproximation());
        aggs.add(new jAudioFeatureExtractor.Aggregators.Mean());
        aggs.add(new jAudioFeatureExtractor.Aggregators.MFCC());
        aggs.add(new jAudioFeatureExtractor.Aggregators.StandardDeviation());
        for (Aggregator agg : aggs)
        {
            aggregatorIndexes.put(agg.getAggregatorDefinition().name, agg);
        }
    }

    /**
     * Used as an argument to List.toArray.
     */
    private static final Aggregator[] aggregatorArrayType = new Aggregator[0];

    /**
     * Used as an argument to List.toArray.
     */
    private static final FeatureExtractor[] featureExtractorArrayType = new FeatureExtractor[0];

    private int windowSize;
    private double windowOverlap;
    private double samplingRate;
    private boolean normalize;
    private boolean[] featuresToExtract;
    private List<Aggregator> aggregators;

    /**
     * Constructs a new SoundProcessorBuilder, initialized to the defaults in {@link Defaults}.
     */
    public SoundProcessorBuilder()
    {
        this.featuresToExtract = new boolean[allFeatures.length];
        this.aggregators = new ArrayList<Aggregator>();
        
        // set defaults
        setWindowSize(Defaults.WINDOW_SIZE);
        setWindowOverlap(Defaults.WINDOW_OVERLAP);
        setSamplingRate(Defaults.SAMPLING_RATE);
        setNormalize(Defaults.NORMALIZE);
        setFeaturesFromProperties(Defaults.FEATURES);
        setAggregatorsFromProperties(Defaults.AGGREGATORS);
    }

    /**
     * @return the windowSize
     */
    public int getWindowSize()
    {
        return windowSize;
    }

    /**
     * @param windowSize
     *            The size of the windows that the audio recordings are to be broken into, in number
     *            of samples. Must be greater than 2.
     */
    public void setWindowSize(int windowSize)
    {
        this.windowSize = windowSize;
    }

    /**
     * @return the windowOverlap
     */
    public double getWindowOverlap()
    {
        return windowOverlap;
    }

    /**
     * @param windowOverlap
     *            The fraction of overlap between adjacent windows. Must be between 0.0 and less
     *            than 1.0, with a value of 0.0 meaning no overlap.
     */
    public void setWindowOverlap(double windowOverlap)
    {
        this.windowOverlap = windowOverlap;
    }

    /**
     * @return the samplingRate
     */
    public double getSamplingRate()
    {
        return samplingRate;
    }

    /**
     * @param samplingRate
     *            The sampling rate that all recordings are to be converted to before feature
     *            extraction, in Hz.
     */
    public void setSamplingRate(double samplingRate)
    {
        this.samplingRate = samplingRate;
    }

    /**
     * @return the normalize
     */
    public boolean isNormalize()
    {
        return normalize;
    }

    /**
     * @param normalize
     *            Whether or not to normalize recordings before feature extraction.
     */
    public void setNormalize(boolean normalize)
    {
        this.normalize = normalize;
    }

    /**
     * @return an unmodifiable list representing the current set of features to be extracted
     */
    public List<FeatureExtractor> getFeatures()
    {
        List<FeatureExtractor> features = new ArrayList<FeatureExtractor>();
        for (int i = 0; i < featuresToExtract.length; i++)
        {
            if (featuresToExtract[i])
            {
                features.add(allFeatures[i]);
            }
        }
        return Collections.unmodifiableList(features);
    }

    /**
     * Sets the features to extract based on a comma-separated list of feature names.
     * 
     * @param names
     *            a comma separated list of features to extract.
     */
    private void setFeaturesFromProperties(String names)
    {
        setFeatures(names.split("\\s*,\\s*"));
    }

    /**
     * Sets the features to extract.
     * 
     * @param features
     *            a list of FeatureExtractors (you can find these in the
     *            jAudioFeatureExtractor.AudioFeatures package)
     */
    public void setFeatures(List<FeatureExtractor> features)
    {
        setFeatures(features.toArray(featureExtractorArrayType));
    }

    /**
     * Sets the features to extract.
     * 
     * @param features
     *            an array of FeatureExtractors (you can find these in the
     *            jAudioFeatureExtractor.AudioFeatures package)
     */
    public void setFeatures(FeatureExtractor... features)
    {
        List<String> featureNames = new ArrayList<String>();
        for (FeatureExtractor feature : features)
        {
            featureNames.add(feature.getFeatureDefinition().name);
        }
        setFeatures(featureNames.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
    }

    /**
     * Sets the features to extract based on feature names.
     * 
     * @param names
     *            an array of feature names. You can get a list of all feature names from
     *            {@link #getAllFeatureNames()}.
     */
    private void setFeatures(String... names)
    {
        resetFeatures();
        for (String name : names)
        {
            int index = featureIndexes.get(name);
            if (index == -1)
            {
                resetFeatures();
                throw new IllegalArgumentException("Bad feature name: " + name);
            }
            this.featuresToExtract[index] = true;
        }
    }

    private void resetFeatures()
    {
        Arrays.fill(featuresToExtract, false);
    }

    /**
     * @return an unmodifiable list of the aggregators
     */
    public List<Aggregator> getAggregators()
    {
        return Collections.unmodifiableList(this.aggregators);
    }

    /**
     * Sets the aggregators based on a comma-separated list of names.
     * 
     * @param names
     *            a comma-separated list of names of aggregators to set. You can get a list of all
     *            aggregator names from {@link #getAllAggregatorNames()}.
     * 
     */
    private void setAggregatorsFromProperties(String names)
    {
        List<Aggregator> aggregators = new ArrayList<Aggregator>();
        for (String agg : names.split("\\s*,\\s*"))
        {
            Aggregator aggregator = aggregatorIndexes.get(agg);
            if (aggregator == null)
            {
                throw new IllegalArgumentException("Bad Aggregator name: " + agg);
            }
            aggregators.add(aggregator);
        }
        setAggregators(aggregators);
    }

    /**
     * Sets the aggregators.
     * 
     * @param aggregators
     *            the aggregators to set. You can find Aggregators in the
     *            jAudioFeatureExtractor.Aggregators package.
     */
    public void setAggregators(Aggregator... aggregators)
    {
        setAggregators(Arrays.asList(aggregators));
    }

    /**
     * Sets the aggregators.
     * 
     * @param aggregators
     *            the aggregators to set. You can find Aggregators in the
     *            jAudioFeatureExtractor.Aggregators package.
     */
    public void setAggregators(List<Aggregator> aggregators)
    {
        this.aggregators = new ArrayList<Aggregator>(aggregators);
    }

    /**
     * Set properties on this SoundProcessorBuilder. All the property keys can be found in
     * {@link Props}.
     * 
     * @param props
     *            a properties object with properties set for the SoundProcessorBuilder. If any
     *            properties are missing, then the values in {@link Defaults} will be used.
     */
    public void setProperties(Properties props)
    {
        String windowSizeS = props.getProperty(Props.WINDOW_SIZE);
        String windowOverlapS = props.getProperty(Props.WINDOW_OVERLAP);
        String samplingRateS = props.getProperty(Props.SAMPLING_RATE);
        String normalizeS = props.getProperty(Props.NORMALIZE);
        String aggregatorsS = props.getProperty(Props.AGGREGATORS);
        String featuresS = props.getProperty(Props.FEATURES);

        int windowSize = (windowSizeS == null) ? Defaults.WINDOW_SIZE : Integer
                .parseInt(windowSizeS);
        double windowOverlap = (windowOverlapS == null) ? Defaults.WINDOW_OVERLAP : Double
                .parseDouble(windowOverlapS);
        double samplingRate = (samplingRateS == null) ? Defaults.SAMPLING_RATE : Double
                .parseDouble(samplingRateS);
        boolean normalize = (normalizeS == null) ? Defaults.NORMALIZE : Boolean
                .parseBoolean(normalizeS);
        String aggregatorsString = (aggregatorsS == null) ? Defaults.AGGREGATORS : aggregatorsS;
        String featuresString = (featuresS == null) ? Defaults.FEATURES : featuresS;

        setWindowSize(windowSize);
        setWindowOverlap(windowOverlap);
        setSamplingRate(samplingRate);
        setNormalize(normalize);
        setAggregatorsFromProperties(aggregatorsString);
        setFeaturesFromProperties(featuresString);
    }

    /**
     * @return a new SoundProcessor instance with the settings from this builder
     */
    public SoundProcessor build()
    {
        AggregatorContainer container = new AggregatorContainer();
        try
        {
            container.add(this.aggregators.toArray(aggregatorArrayType));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Unable to initialize AggregatorContainer in "
                    + this.getClass().getName());
        }
        SoundProcessor featureProcessor = new SoundProcessor(this.windowSize, this.windowOverlap,
                this.samplingRate, this.normalize, allFeatures, this.featuresToExtract, container);
        return featureProcessor;
    }

    public static Collection<String> getAllFeatureNames()
    {
        return featureIndexes.keySet();
    }

    public static Collection<String> getAllAggregatorNames()
    {
        return aggregatorIndexes.keySet();
    }

    /**
     * Defaults values for SoundProcessorBuilder.
     */
    public class Defaults
    {
        public static final int WINDOW_SIZE = 256;
        public static final double WINDOW_OVERLAP = 0.25;
        public static final double SAMPLING_RATE = 8000;
        public static final boolean NORMALIZE = false;
        public static final String AGGREGATORS = "Mean, Standard Deviation";
        public static final String FEATURES = "Power Spectrum, Spectral Centroid";
    }

    /**
     * Property keys that can be used in a call to
     * {@link SoundProcessorBuilder#setProperties(Properties)}.
     */
    public class Props
    {
        private static final String PREFIX = "soundprocessor.";
        public static final String WINDOW_SIZE = PREFIX + "windowSize";
        public static final String WINDOW_OVERLAP = PREFIX + "windowOverlap";
        public static final String SAMPLING_RATE = PREFIX + "samplingRate";
        public static final String NORMALIZE = PREFIX + "normalize";
        public static final String AGGREGATORS = PREFIX + "aggregators";
        public static final String FEATURES = PREFIX + "features";
    }
}
